using System;
using System.Collections.Generic;
using System.IO;
#if TEST
using Xunit;
#endif

namespace Fadd.Parser
{
    /// <summary>
    /// Converts a string to a token tree.
    /// </summary>
    public class Tokenizer
    {
        private readonly List<ParseTreeToken> _prototypes = new List<ParseTreeToken>();

        private string _text;
        private int _currentPos = -1;

        /// <summary>
        /// Parses the specified stream.
        /// </summary>
        /// <param name="stream">Stream with text content.</param>
        /// <param name="tree">Tree that will be filled with tokens.</param>
        public void Parse(Stream stream, ParseTree tree)
        {
            StreamReader reader = new StreamReader(stream);
            Parse(reader.ReadToEnd(), tree);
        }

        /// <summary>
        /// Parses the specified text.
        /// </summary>
        /// <param name="text">Text to parse.</param>
        /// <param name="tree">Tree that will be filled with tokens.</param>
        /// <exception cref="NotSupportedException">When we find unsupported tokens.</exception>
        public void Parse(string text, ParseTree tree)
        {
            _text = text.Trim();
            ParseTreeToken currentToken = null;
            while (!PeekEOF)
            {
                bool found = false;
                foreach (ParseTreeToken token in _prototypes)
                {
                    if (PeekEOF)
                        break;
                    if (!token.Match(this, tree)) 
                        continue;

                    token.Parse(this, tree);
                    found = true;
                    currentToken = token;
                    break;
                }

                // Now try to parse childen of the current node.
                if (currentToken != null)
                {
                    tree.IntoNode();
                    bool res = currentToken.ParseOneChild(this, tree);
                    tree.OutOfNode();
                    if (res)
                        continue;
                }

                if (!found)
                {
                    ++_currentPos;
                    throw new NotSupportedException("Failed to find token at position: " + _currentPos + " [" + text.Substring(_currentPos) + "] Complete string: '" + text + "'.");
                }
            }
        }

        /// <summary>
        /// Prototypes are used to 
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        public ParseTreeToken GetPrototype(Type type)
        {
            foreach (ParseTreeToken token in _prototypes)
            {
                if (type.IsAssignableFrom(token.GetType()))
                    return token;
            }

            return null;
        }

        /// <summary>
        /// Get a string for the next <paramref name="count"/> chars.
        /// </summary>
        /// <param name="count">Number of chars to peek.</param>
		/// <returns>A string with <paramref name="count"/> characters.</returns>
        public string LookAhead(int count)
        {
            return _currentPos + 1 + count < _text.Length ? _text.Substring(_currentPos + 1, count) : null;
        }

        /// <summary>
        /// Get a string for the next <paramref name="count"/> chars.
        /// </summary>
        /// <param name="count">Number of chars to peek</param>
        /// <param name="ignoreCharacters">Ignore these characters before starting to count</param>
		/// <returns>A string with <paramref name="count"/> characters.</returns>
        public string LookAhead(int count, string ignoreCharacters)
        {
            int pos = PeekIgnore(ignoreCharacters);
            return pos + count <= _text.Length ? _text.Substring(pos, count) : null;
        }

        /// <summary>
        /// Get previous character
        /// </summary>
        /// <returns>A <see cref="char"/></returns>
        /// <remarks>Will not change the current position</remarks>
        public char Previous()
        {
            return _currentPos == 0 ? char.MinValue : _text[_currentPos - 1];
        }

        /// <summary>
        /// Scroll back <paramref name="steps"/> and get that char.
        /// </summary>
        /// <param name="steps">Number of chars back</param>
        /// <returns>A <see cref="char"/></returns>
        /// <remarks>Will not change the current position</remarks>
        public char Previous(int steps)
        {
            ++steps;
            return _currentPos - steps < 0 ? char.MinValue : _text[_currentPos - steps];
        }

        /// <summary>
        /// Peek forward on a char
        /// </summary>
        /// <param name="steps">Which char index (from current position) to get.</param>
        /// <returns>A <see cref="char"/>.</returns>
        public char Peek(int steps)
        {
            ++steps;
            return _currentPos + steps < _text.Length ? _text[_currentPos + steps] : char.MinValue;
        }

        /// <summary>
        /// Peek forward on a char
        /// </summary>
        /// <param name="ignoreChars">Ignore these characters before string to peek</param>
        /// <param name="steps">Which char index (from current position) to get.</param>
        /// <returns>A <see cref="char"/>.</returns>
        public char Peek(string ignoreChars, int steps)
        {
            int pos = PeekIgnore(ignoreChars);
            steps += pos;
            return steps < _text.Length ? _text[steps] : char.MinValue;
        }

        /// <summary>
        /// Get next position that do not contain the specified chars.
        /// </summary>
        /// <param name="ignoreChars"></param>
        /// <returns></returns>
        private int PeekIgnore(string ignoreChars)
        {
            int pos = _currentPos + 1;
            while (pos < _text.Length && ignoreChars.IndexOf(_text[pos]) != -1)
                ++pos;
            return pos;
        }

#if TEST
        [Fact]
        private void TestPeekIgnoreCharsSteps1()
        {
            _text = "       |\r\n";
            char ch = Peek(" \t", 1);
            Assert.Equal('\r', ch);
        }

        [Fact]
        private void TestPeekIgnoreCharsSteps2()
        {
            _text = "       |\r\n";
            char ch = Peek(" \t", 3);
            Assert.Equal(char.MinValue, ch);
        }
#endif

        /// <summary>
        /// Peek on the next character
        /// </summary>
        /// <returns>The next character</returns>
        /// <exception cref="InvalidOperationException">If trying to peek beyond last character in the string.</exception>
        public char Peek()
        {
            if (_currentPos + 1 < _text.Length)
                return _text[_currentPos + 1];

            throw new InvalidOperationException("Peeking beyond end of file.");
        }

        /// <summary>
        /// Peek forward
        /// </summary>
        /// <param name="throwException">true if extensions should be thrown</param>
        /// <returns><see cref="char.MinValue"/> if no extensions should be thrown and we are peeking beyond end of text.</returns>
        public char Peek(bool throwException)
        {
            if (throwException)
                return Peek();

            return _currentPos + 1 < _text.Length ? _text[_currentPos + 1] : char.MinValue;
        }

        /// <summary>
        /// Peek at the next character.
        /// </summary>
        /// <param name="ignoreChars">Characters that are ignored (not counted).</param>
        /// <returns>the next character, or <see cref="char.MinValue"/> if no more characters exist.</returns>
        public char Peek(string ignoreChars)
        {
            int pos = _currentPos + 1;
            while (pos < _text.Length && ignoreChars.IndexOf(_text[pos]) != -1)
                ++pos;

            return pos < _text.Length ? _text[pos] : char.MinValue;
        }

        /// <summary>
        /// Read X number of characters
        /// </summary>
        /// <param name="length">number of characters to read</param>
        /// <returns>A string</returns>
        /// <remarks>Moves current position forward.</remarks>
        public string Read(int length)
        {
            if (_currentPos + length + 1 <= _text.Length)
            {
                string text = _text.Substring(_currentPos + 1, length);
                _currentPos += length;
                return text;
            }

            return string.Empty;
        }

        /// <summary>
        /// Read a character
        /// </summary>
        /// <returns>A char</returns>
        /// <remarks>Moves current position forward.</remarks>
        public char Read()
        {
            if (_currentPos + 1 < _text.Length)
            {
                ++_currentPos;
                return _text[_currentPos];
            }

            return char.MinValue;
        }

        /// <summary>
        /// Ignore all specified chars
        /// </summary>
        /// <param name="ignoreCharacters">chars to ignore</param>
        /// <returns>true if end of text is not hit.</returns>
        /// <remarks>Moves current position forward.</remarks>
        public bool Ignore(string ignoreCharacters)
        {
            if (_currentPos + 1 >= _text.Length)
                return false;

            while (_currentPos + 1 < _text.Length && ignoreCharacters.IndexOf(_text[_currentPos + 1]) != -1)
                ++_currentPos;

            return _currentPos < _text.Length;
        }

        /// <summary>
        /// Returns index of current position as a string.
        /// </summary>
        public string CurrentPosition
        {
            get { return _currentPos.ToString(); }
        }

        /// <summary>
        /// All prototype tokens.
        /// </summary>
        public List<ParseTreeToken> Prototypes
        {
            get { return _prototypes; }
        }

        /// <summary>
        /// We are at end of text
        /// </summary>
        public bool EOF
        {
            get { return _currentPos >= _text.Length; }
        }

        /// <summary>
        /// Next character is the last one.
        /// </summary>
        public bool PeekEOF
        {
            get { return _currentPos + 1>= _text.Length; }
        }
    }
}
